<?php

class Template
{
	// class constants
	const PRINT_RENDERING = 0;
	const RETURN_RENDERING = 1;
	const GATHERING = 0;
	const RENDERING = 1;
	
	// instance properties
	private $templateDirectory;
	private $variableList;
	private $currentTemplate;
	private $mode;
	private $extensionPrefix;
	private $templateList;
	
	// instance methods
	// constructor
	public function __construct($templateDirectory = "", $variableList = array())
	{
		$this->templateDirectory = $templateDirectory;
		$this->variableList = $variableList;
		$this->currentTemplate = "";
		$this->mode = null;
		$this->extensionPrefix = "";
		$this->templateList = array();
	}
	
	public function initialise($renderMode, $templateName)
	{
		try
		{
			Block::setMode(Block::GATHER);
			$this->mode = self::GATHERING;
			Buffer::start();
			$this->inheritFrom($templateName);
			Buffer::endClean();
			Block::setMode(Block::RENDER);
			$this->mode = self::RENDERING;
			Buffer::start();
			$this->produceOutput($this->currentTemplate, $this->variableList);
			$content = Buffer::getClean();
			if ($renderMode == self::PRINT_RENDERING)
			{
				echo $content;
				return;
			}
			return $content;
		}
		catch (Template_NonexistentTemplateException $e)
		{
			throw new Template_Exception($e->getMessage(), 0, $e);
		}
		catch (Exception $e)
		{
			throw new Template_Exception($e->getMessage(), 0, $e);
		}
	}
	
	public function inheritFrom($templateName)
	{
		$path = "";
		$templateFilename = $this->getTemplateFilenameFromName($templateName);
		if (!$this->findTemplate($templateName, $path))
		{
			throw new Template_NonexistentTemplateException(sprintf("Template %s (%s) could not be found", $templateFilename, $path));
		}
		
		$this->currentTemplate = $path;
		$this->templateList[] = $path;
		$this->produceOutput($path, $this->getVariableList());
	}
	
	// template resolution methods
	public function findTemplate($templateName, &$pathOut = "")
	{
		$path = $this->getPathFromTemplateName($templateName);
		if (!file_exists($path))
		{
			return false;
		}
		
		$pathOut = $path;
		return true;
	}
	
	private function getPathFromTemplateName($templateName)
	{
		return implode(DIRECTORY_SEPARATOR, array($this->getTemplateDirectory(), $this->getTemplateFilenameFromName($templateName)));
	}
	
	private function getTemplateFilenameFromName($templateName)
	{
		$filename = sprintf("%s.phtml", $templateName);
		$extensionPrefix = $this->getExtensionPrefix();
		if (strlen($extensionPrefix) > 0)
		{
			$filename = sprintf("%s.%s.phtml", $templateName, $extensionPrefix);
		}
		
		return $filename;		
	}
	
	/**
	 * Includes the provided path with the variables extract()'d accordingly
	 * 
	 * Isolating the variable list provided such that any other methods do not introduce irrelevent
	 * variables into the local scope of the included file.
	 * 
	 * @param string
	 * @param array
	 */
	private function produceOutput($path, $variableList)
	{
		extract($variableList);
		include($path);
	}
	
	// template utility methods
	/**
	 * Formally defines an input required by a template
	 * 
	 * Design-by-contract method of requiring a verifiable interface
	 * to template inputs.
	 * 
	 * @param string
	 * @param string
	 * @throws Template_InputException
	 */
	public function input($inputName, $inputType)
	{
		if ($this->mode == self::GATHERING)
		{
			$backtrace = debug_backtrace();
			$currentTemplate = $backtrace[0]["file"];
			$variableList = $this->variableList;
			if (!in_array($inputName, array_keys($variableList)))
			{
				throw new Template_InputException(sprintf('Variable "%s" is required by template "%s" and could not be found', $inputName, $currentTemplate));
			}
			
			$inputValue = $variableList[$inputName];
			$inputTypeList = !is_array($inputType) ? array($inputType) : $inputType;
			$hasValidInputType = false;
			foreach ($inputTypeList as $type)
			{
				$typeFound = $this->getInputType($inputValue, $type);
				if ($typeFound === $type)
				{
					$hasValidInputType = true;
					
					break;
				}
			}
			
			if ($hasValidInputType === false)
			{
				$pattern = 'Variable "%s" in template "%s" is required to be of the types "%s", but was "%s"';
				if (!is_array($inputType))
				{
					$pattern = 'Variable "%s" in template "%s" is required to be of the type "%s", but was "%s"'; 
				}
				throw new Template_InputException(sprintf($pattern, $inputName, $currentTemplate, implode(", ", $inputTypeList), $typeFound));
			}
		}
	}
	
	/**
	 * Returns input type based on input
	 * 
	 * Returns input type based on input, checks resulting
	 * input type against expected type to include user-
	 * defined types.
	 * 
	 * @param string
	 * @param string
	 * @return string
	 */
	private function getInputType($inputValue, $inputType)
	{
		$typesFound = array(
			// scalar
			"string" => is_string($inputValue),
			"int" => is_int($inputValue),
			"float" => is_float($inputValue),
			"bool" => is_bool($inputValue),
			
			// vector
			"array" => is_array($inputValue),
			"object" => is_object($inputValue),
			
			// special
			"resource" => is_resource($inputValue),
			"null" => is_null($inputValue)
		);
		$typeFound = array_search(true, $typesFound);
		if ($typeFound != $inputType && $typeFound == "object")
		{
			$typeFound = get_class($inputValue);
		}
		
		return $typeFound;
	}
	
	// instance property getters
	public function getVariableList()
	{
		return $this->variableList;
	}
	
	public function setVariableList(array $variableList)
	{
		$this->variableList = $variableList;
		
		return $this;
	}
	
	public function getExtensionPrefix()
	{
		return $this->extensionPrefix;
	}
	
	public function setExtensionPrefix($extensionPrefix)
	{
		$this->extensionPrefix = $extensionPrefix;
		
		return $this;
	}
	
	public function getTemplateDirectory()
	{
		return $this->templateDirectory;
	}
	
	public function setTemplateDirectory($templateDirectory)
	{
		$this->templateDirectory = $templateDirectory;
		
		return $this;
	}
	
	public function getTemplateList()
	{
		return $this->templateList;
	}
}